#include "dir.h"
#include <utility>
#include <stack>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <dirent.h>


namespace evm { namespace fs {

template<class T, class U = T>
T exchange(T& obj, U&& new_value) {
    T old_value = std::move(obj);
    obj = std::forward<U>(new_value);
    return old_value;
}

struct _Dir {
    _Dir() : dirp(nullptr) {}

    _Dir(DIR* dirp, const fs::path& path) : dirp(dirp), path(path) {}

    _Dir(_Dir &&d)
        : dirp(exchange(d.dirp, nullptr)), path(std::move(d.path)),
          entry(std::move(d.entry)), type(d.type) {}

    _Dir& operator=(_Dir&&) = delete;

    ~_Dir() { if (dirp) ::closedir(dirp); }

    bool advance(std::error_code*, directory_options = directory_options::none);

    DIR* dirp;
    fs::path path;
    directory_entry entry;
    file_type type = file_type::none;
};

namespace {

template<typename Bitmask>
inline bool is_set(Bitmask obj, Bitmask bits) {
    return (obj & bits) != Bitmask::none;
}

// Returns {dirp, p} on success, {nullptr, p} on error.
// If an ignored EACCES error occurs returns {}.
inline _Dir open_dir(const path &p, directory_options options, std::error_code *ec) {
    if (ec)
        ec->clear();

    if (DIR *dirp = ::opendir(p.c_str()))
        return {dirp, p};

    const int err = errno;
    if (err == EACCES && is_set(options, directory_options::skip_permission_denied))
        return {};

    if (!ec)
        throw filesystem_error("directory iterator cannot open directory", p,
                               std::error_code(err, std::generic_category()));

    ec->assign(err, std::generic_category());
    return {nullptr, p};
}

inline file_type get_file_type(const ::dirent &d) {
    switch (d.d_type) {
        case DT_BLK:
            return file_type::block;
        case DT_CHR:
            return file_type::character;
        case DT_DIR:
            return file_type::directory;
        case DT_FIFO:
            return file_type::fifo;
        case DT_LNK:
            return file_type::symlink;
        case DT_REG:
            return file_type::regular;
        case DT_SOCK:
            return file_type::socket;
        case DT_UNKNOWN:
            return file_type::unknown;
        default:
            return file_type::none;
    }
}

}

// Returns false when the end of the directory entries is reached.
// Reports errors by setting ec or throwing.
bool _Dir::advance(error_code *ec, directory_options options) {
    if (ec)
        ec->clear();

    int err = exchange(errno, 0);
    const auto entp = ::readdir(dirp);
    std::swap(errno, err);

    if (entp) {
        // skip past dot and dot-dot
        if (!strcmp(entp->d_name, ".") || !strcmp(entp->d_name, ".."))
            return advance(ec, options);
        entry = directory_entry{path / entp->d_name};
        type = get_file_type(*entp);
        return true;
    }
    else if (err) {
        if (err == EACCES && is_set(options, directory_options::skip_permission_denied))
            return false;

        if (!ec)
            throw filesystem_error("directory iterator cannot advance",
                                   std::error_code(err, std::generic_category()));
        ec->assign(err, std::generic_category());
        return true;
    }
    else {
        // reached the end
        entry = {};
        type = file_type::none;
        return false;
    }
}

directory_iterator::directory_iterator(const path &p, directory_options options, error_code *ec) {
    _Dir dir = open_dir(p, options, ec);

    if (dir.dirp) {
        auto sp = std::make_shared<_Dir>(std::move(dir));
        if (sp->advance(ec, options))
            dir_.swap(sp);
    }
    else if (!dir.path.empty()) {
        // An error occurred, we need a non-empty shared_ptr so that *this will
        // not compare equal to the end iterator.
        dir_.reset(static_cast<_Dir *>(nullptr));
    }
}

const directory_entry &directory_iterator::operator*() const {
    if (!dir_)
        throw filesystem_error("non-dereferenceable directory iterator",
                               std::make_error_code(errc::invalid_argument));
    return dir_->entry;
}

directory_iterator&
directory_iterator::operator++() {
    if (!dir_)
        throw filesystem_error("cannot advance non-dereferenceable directory iterator",
                               std::make_error_code(errc::invalid_argument));
    if (!dir_->advance(nullptr))
        dir_.reset();
    return *this;
}

directory_iterator&
directory_iterator::increment(error_code &ec) noexcept {
    if (!dir_) {
        ec = std::make_error_code(errc::invalid_argument);
        return *this;
    }
    if (!dir_->advance(&ec))
        dir_.reset();
    return *this;
}

using Dir_iter_pair = std::pair<_Dir, directory_iterator>;

struct recursive_directory_iterator::_Dir_stack : std::stack<_Dir> {
    void clear() { c.clear(); }
};

recursive_directory_iterator::
recursive_directory_iterator(const path &p, directory_options options, error_code *ec)
: options_(options), pending_(true) {
    if (DIR *dirp = ::opendir(p.c_str())) {
        auto sp = std::make_shared<_Dir_stack>();
        sp->push(_Dir{dirp, p});
        if (sp->top().advance(ec))
            dirs_.swap(sp);
    }
    else {
        const int err = errno;
        if (err == EACCES
            && is_set(options, directory_options::skip_permission_denied)) {
            if (ec)
                ec->clear();
            return;
        }

        if (!ec)
            throw filesystem_error("recursive directory iterator cannot open directory", p,
                                   std::error_code(err, std::generic_category()));

        ec->assign(err, std::generic_category());

        // An error occurred, we need a non-empty shared_ptr so that *this will
        // not compare equal to the end iterator.
        dirs_.reset(static_cast<_Dir_stack *>(nullptr));
    }
}

recursive_directory_iterator::~recursive_directory_iterator() = default;

int recursive_directory_iterator::depth() const {
    return int(dirs_->size()) - 1;
}

const directory_entry&
recursive_directory_iterator::operator*() const {
    return dirs_->top().entry;
}

recursive_directory_iterator&
recursive_directory_iterator::
operator=(const recursive_directory_iterator &) noexcept = default;

recursive_directory_iterator&
recursive_directory_iterator::
operator=(recursive_directory_iterator &&) noexcept = default;

recursive_directory_iterator&
recursive_directory_iterator::operator++() {
    error_code ec;
    increment(ec);
    if (ec.value())
        throw filesystem_error("cannot increment recursive directory iterator", ec);
    return *this;
}

namespace {

bool recurse(const _Dir &d, directory_options options, std::error_code &ec) {
    bool follow_symlink = is_set(options, directory_options::follow_directory_symlink);

    fs::path path = d.entry.path();
    auto type = symlink_status(path.c_str(), ec).type();
    if (ec.value())
        return false;

    if (type == file_type::symlink) {
        if (!follow_symlink)
            return false;

        type = status(path, ec).type();  // follow symlink
    }

    return type == file_type::directory;
}

}

recursive_directory_iterator&
recursive_directory_iterator::increment(error_code &ec) noexcept {
    if (!dirs_) {
        ec = std::make_error_code(errc::invalid_argument);
        return *this;
    }

    auto &top = dirs_->top();

    if (exchange(pending_, true) && recurse(top, options_, ec)) {
        _Dir dir = open_dir(top.entry.path(), options_, &ec);
        if (ec)
            return *this;
        if (dir.dirp)
            dirs_->push(std::move(dir));
    }

    while (!dirs_->top().advance(&ec, options_) && !ec) {
        dirs_->pop();
        if (dirs_->empty()) {
            dirs_.reset();
            return *this;
        }
    }
    return *this;
}

void recursive_directory_iterator::pop() {
    if (!dirs_)
        throw filesystem_error("cannot pop non-dereferenceable recursive directory iterator",
                               std::make_error_code(errc::invalid_argument));

    do {
        dirs_->pop();
        if (dirs_->empty()) {
            dirs_.reset();
            return;
        }
    } while (!dirs_->top().advance(nullptr, options_));
}

}}
